昨天我們確認了 Ollama 能連得上,拿到一句話的回答。但一句話沒辦法直接存進資料庫,也沒辦法讓程式知道「這句話裡哪個是主題、哪個是預估時數」。
今天要解決這件事:讓本機模型直接回傳「照格式排好的資料」,而不是一段要人工解析的文字。這是 Day 13 寫 Planner Agent 的地基,先在這裡把「怎麼拿到乾淨的結構化資料」練熟。
直接請模型用文字回答,你會拿到類似這樣的東西:
我建議你今天學習 EC2 的基礎概念,因為這是後續服務的基礎,大概需要 2 小時。
這對人很好讀,但程式沒辦法直接用。程式需要的是:
{"topic": "EC2 基礎概念", "reason": "後續服務的基礎", "estimated_hours": 2}
想像成點餐:跟服務生說「我要一份套餐,附薯條跟可樂」,廚房聽得懂但沒辦法直接下單;點餐機把它拆成「主餐、附餐、飲料」三個欄位,廚房才能照著做。結構化回應就是把模型的答案拆進固定欄位,讓後面的資料庫、API 都能直接使用。
Day 10 開始要用 LangGraph 組出 Agent 的流程,LangGraph 本身是建立在 LangChain 之上的。今天先透過 langchain-ollama 提供的 ChatOllama,熟悉這一層的用法,Day 10 寫 Node 的時候會直接沿用今天的東西。
ChatOllama 底層還是呼叫本機的 Ollama 服務,只是多包了一層方便串接 LangGraph 和結構化輸出的介面。
小提醒:開源模型在「照格式輸出」這件事上,穩定度通常不如大型商用模型,llama3.1:8b 已經支援工具呼叫(tool calling),能勝任結構化輸出,但偶爾還是會輸出格式跑掉。這也是今天要順便加上重試機制的原因。
cd backend
python -m pip install langchain langchain-ollama
pydantic 已經隨 FastAPI 裝好了,不用再裝一次。
驗證:
python -c "from langchain_ollama import ChatOllama; print('安裝成功')"
用 Pydantic 描述「希望模型回什麼欄位」,Field 裡的 description 會被當成提示的一部分,告訴模型每個欄位該填什麼。
檔案位置: backend/schemas.py
狀態: 修改檔案(在 Day 7 的內容後面加上這段)
用途: 新增本機模型結構化回應會用到的格式
依賴: pydantic
class StudyAdvice(BaseModel):
"""模型針對今日學習給出的建議"""
topic: str = Field(description="今天建議學習的主題")
reason: str = Field(description="為什麼推薦這個主題")
estimated_hours: float = Field(gt=0, description="預估需要花費的時數")
檔案位置: backend/ollama_client.py
狀態: 新增檔案
用途: 封裝 Ollama 本機模型呼叫,提供結構化回應與重試機制
依賴: langchain-ollama, schemas
from langchain_ollama import ChatOllama
from schemas import StudyAdvice
# temperature 調低一點,讓結構化輸出更穩定、不容易亂跑格式
_llm = ChatOllama(model="llama3.1:8b", temperature=0.3)
# with_structured_output:讓輸出直接符合 StudyAdvice 的格式
# with_retry:輸出格式偶爾跑掉時自動重試,最多 3 次
_structured_llm = _llm.with_structured_output(StudyAdvice).with_retry(
stop_after_attempt=3
)
def get_study_advice(user_message: str) -> StudyAdvice:
"""根據使用者輸入,請本機模型給出結構化的學習建議"""
return _structured_llm.invoke(user_message)
with_structured_output(StudyAdvice) 是關鍵:LangChain 會把 StudyAdvice 的欄位定義轉成模型看得懂的格式要求,模型回傳後也會自動解析、驗證成 StudyAdvice 物件,不用自己寫 JSON 解析和格式檢查。
檔案位置: backend/test_ollama_client.py
狀態: 新增檔案
用途: 驗證 ollama_client.py 能重複拿到正確格式的結構化回應
依賴: ollama_client
from ollama_client import get_study_advice
def main() -> None:
"""呼叫兩次,確認結構化輸出穩定、可重複"""
for i in range(1, 3):
advice = get_study_advice(
"我正在準備 AWS Solutions Architect 認證,目前是初學者,"
"已經花了一週讀 EC2,接下來建議學什麼?"
)
print(f"第 {i} 次呼叫:")
print(f" 主題:{advice.topic}")
print(f" 原因:{advice.reason}")
print(f" 預估時數:{advice.estimated_hours}")
if __name__ == "__main__":
main()
執行:
python test_ollama_client.py
第一次執行可能要等模型「暖機」(Ollama 第一次呼叫某個模型時要把它載進記憶體),會比第二次呼叫慢一些。應該看到兩次呼叫都回傳有 topic、reason、estimated_hours 三個欄位的乾淨結果,即使兩次問的內容一樣,答案的用詞也可能不完全一樣,但格式一定固定。
advice 是一個 StudyAdvice 物件,不是字典,所以打錯欄位名稱會被 Python 直接抓到。這步只是驗證用,不用寫進任何檔案,跟著下面的步驟在終端機操作就好。
1. 確認在 backend/ 目錄下
打開終端機,確認路徑在 backend/(跟 ollama_client.py 同一層):
cd backend
2. 進入互動式 Python
輸入:
python
按 Enter 後,畫面會變成這樣,代表已經進入互動模式,等你輸入指令:
>>>
3. 匯入剛剛寫好的函式
在 >>> 後面貼上這一行,按 Enter:
from ollama_client import get_study_advice
沒有出現任何錯誤訊息就是成功了。
4. 呼叫一次,拿到結果
繼續貼上這一行,按 Enter(這一步會實際呼叫 Ollama,需要等幾秒):
advice = get_study_advice("推薦我今天學什麼")
5. 故意打錯欄位名稱
貼上這一行,注意 estimated_hour 少了最後的 s,這是刻意的:
advice.estimated_hour
按 Enter 後應該看到:
AttributeError: 'StudyAdvice' object has no attribute 'estimated_hour'
看到這個錯誤,就代表 Pydantic 定義的格式是真的在把關資料型別,而不是只是好看的型別提示。如果改打正確的欄位名稱 advice.estimated_hours(沒有打錯),會正常印出一個數字,不會報錯。
6. 離開互動模式
驗證完輸入:
exit()
回到一般的終端機畫面即可,不需要保留或刪除任何檔案。
with_structured_output 回傳的內容跟預期不符,甚至報錯格式錯誤開源模型比商用模型更容易「不照格式」。把 Field 的 description 寫得更具體,例如把「主題」改成「一個具體的技術概念名稱,不要用整個章節當作主題」,通常就能改善。如果還是不穩定,可以把 temperature 調更低(例如 0.1),輸出會更保守、格式更穩定。
先確認 Day 8 的 test_ollama.py 還能正常跑,排除 Ollama 服務本身沒開或模型沒下載的問題。如果基本連線沒問題,單純是結構化輸出不穩定,可以考慮換 qwen2.5:7b 試試看,不同模型對格式的服從度不一樣。
可以,但那樣要自己寫 json.loads()、自己檢查欄位齊不齊全、型別對不對,模型只要漏一個逗號整段就解析失敗。with_structured_output 把這些都包好了,而且 Day 10 開始要用 LangGraph 組 Agent 流程,ChatOllama 正好是 LangGraph 期待的介面,兩邊銜接不用額外轉換。
ModuleNotFoundError: No module named 'langchain_ollama'確認用 python -m pip install langchain langchain-ollama 裝在跟執行腳本相同的 Python 環境。
可以,StudyAdvice 只是範例格式。Day 13 寫 Planner Agent 時,會定義一個更大的 Schema 來裝一整週的計畫,做法完全一樣,只是欄位更多。
今天把 Ollama 升級成「能回傳結構化資料」的版本。寫了 ollama_client.py,用 Pydantic 定義好格式,加上自動重試,兩次呼叫都能拿到格式一致、能直接被程式使用的結果。
系統現在是這樣的:
Day 1 ✓ 產品定義完成
Day 2 ✓ 開發環境準備
Day 3 ✓ 專案架構設計
Day 4 ✓ 資料庫設計
Day 5 ✓ SQLite 資料庫建置
Day 6 ✓ FastAPI 基礎
Day 7 ✓ 使用者檔案 API
Day 8 ✓ 理解 LLM Agent 的本質
Day 9 ✓ 連接 Ollama 本機模型(今天)
Day 10 ⬜ LangGraph 最小範例
ollama_client.py 會是接下來所有 Agent 的共同基礎。明天(Day 10)要把它接進 LangGraph,組出第一個真正會跑的 Agent 流程:輸入 → 本機模型 → 輸出。這是這兩週的難點,明天多留一點時間比較保險。